package com.hmdp.common.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import static com.hmdp.common.utils.RedisConstants.CACHE_NULL_TTL;

/**
 * 缓存穿透  缓存null值  布隆过滤器
 * 缓存击穿  逻辑过期   手动缓存热点key并设置不过期
 * 缓存雪崩  过期时间增加随机值
 * @author WDs , wds8.24@outlook.com
 * @version 1.0
 * @since 2023-01-12 16:40
 */
@Component
@Slf4j
public class CacheClient {
    @Resource
    private final StringRedisTemplate stringRedisTemplate;
    private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    public void saveValue(String key, Object value) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value));
    }

    public void saveValue(String key, Object value, Long ttl, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), ttl, unit);
    }

    /**
     * 存储并设置逻辑过期
     * @param key 键名
     * @param value 存储的数据
     * @param ttl 过期时间
     * @param unit 时间单位
     */
    public void saveWithLogicalExpire(String key, Object value, Long ttl, TimeUnit unit) {
        if (value == null) {
            return;
        }
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(ttl)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), ttl, unit);
    }

    /**
     * 存储设置逻辑过期 并解除同步锁
     * @param key 键名
     * @param value 存储的数据
     * @param ttl 过期时间
     * @param unit 时间单位
     * @param lockKey 锁名字
     */
    private void saveLogicalExpireAndUnlock(String key, Object value, Long ttl, TimeUnit unit,String lockKey) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(ttl)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), ttl, unit);
        unlock(lockKey);
    }

    /**
     * 防止缓存穿透的查询方法 （缓存空值）
     * @param key  redis键名
     * @param ttl 缓存时间
     * @param unit 时间单位
     * @param Type 返回值类型 .class
     * @param dbFallBack  查询数据库的逻辑
     * @return type类型的实体   未查到（null）
     * @param <R> 返回值类型
     * @param <I> ID的类型
     */

    public <R, I> R queryWithPassThrough(String key, Class<R> Type, Supplier<R> dbFallBack, Long ttl, TimeUnit unit) {
        String json = stringRedisTemplate.opsForValue().get(key);
//      如果是缓存的空值
        if ("".equals(json)){
            return null;
        }
//          命中真实数据，返回 并刷新缓存有效期
        if (StrUtil.isNotBlank(json)) {
            R cacheResult = JSONUtil.toBean(json, Type);
            //刷新缓存时间
            stringRedisTemplate.expire(key, ttl, unit);
            return cacheResult;
        }
//      未命中 查询数据库
        R dbResult = dbFallBack.get();
        if (dbResult == null) {
//          缓存空值      解决缓存穿透的方法  缓存空值和布隆过滤器
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        } else {
//          缓存数据
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(dbResult), ttl, unit);
        }
        return dbResult;
    }

    /**
     * 通过 redis的setNX实现分布式互斥锁
     * @return 是否获取成功
     */
    private boolean tryGetLock(String lock) {
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lock, lock, 10, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(aBoolean);
    }

    /**
     * 释放锁
     */
    private void unlock(String lock) {
        stringRedisTemplate.delete(lock);
    }

    /**
     * 逻辑过期查询   如果redis中没有记录将不会查询数据库 需提前预热
     * @param key 键名
     * @param type 查询的数据类型
     * @param dbQuery 刷新缓存是执行的数据库查询操作
     * @param ttl 逻辑过期时间
     * @param unit 时间单位
     * @return R对象  空为null
     * @param <R> 查询的数据类型
     */
    public  <R> R queryWithLogicalExpire(String key, Class<R> type, Supplier<R> dbQuery, Long ttl, TimeUnit unit) {
//      查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json == null) {
            return null;
        }
//      判断过期
        RedisData data = JSONUtil.toBean(json, RedisData.class);
        if (data.getExpireTime().isAfter(LocalDateTime.now())) {
//          过期   获取互斥锁，独立线程更新缓存
            String lockKey = "lock:" + key;
            if (tryGetLock(lockKey)) {
//              再次校验   如果有其他线程完成了刷新操作将不再继续
                json = stringRedisTemplate.opsForValue().get(key);
                data = JSONUtil.toBean(json, RedisData.class);
                if (LocalDateTime.now().isBefore(data.getExpireTime())) {
                    return JSONUtil.toBean((JSONObject) data.getData(), type);
                }
//              提交刷新任务到线程池
                EXECUTOR_SERVICE.submit(() -> saveLogicalExpireAndUnlock(key,dbQuery.get(),ttl,unit,lockKey));
            }
        }
//      未过期 或者 已经过期并执行缓存 或者 已过期已有缓存任务 直接返回当前查询出的数据
        return  JSONUtil.toBean((JSONObject) data.getData(), type);
    }




}
